
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.PrintStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileNotFoundException;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.lang.NumberFormatException;
import java.lang.Thread;
import java.util.ArrayList;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.DOMException;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException; 

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

class Nodo{

  /**
   * In case this file is executed through the command line, the main procedure
   * exists to initialize a Nodo correctly. After data is read from standard
   * input, a new Nodo is instantiated with parameters specified by user
   * 
   * nodo -p <puerto> -c <conocidos> -b <biblioteca> -i <ID del nodo>
   * 
   * @param args Options specified by user
   */
  public static void main(String args[]){

    try{
      System.out.println("Iniciando el nodo, por favor espere...");
      Nodo n = createNodo(System.err, args);

      System.out.println(n.toString());
      System.out.println("Nodo iniciado con éxito");

      n.hear();
    }catch (NodoException e){
      System.err.println("Error de ejecución: ");
      System.err.println(e.getMessage());
      return;
    }
  }

  /**
   * Given the array of options, validate and load options
   * 
   * @param args Array of options given in command line
   */
  public static Nodo createNodo(PrintStream err, String args[])
    throws NodoException
  {

    String options[] = {null, null, null, null};

    for (int i = 0; i < args.length; ++ i){
      if ("-p".equals(args[i])){
        options[0] = args[++ i];
      }else if ("-c".equals(args[i])){
        options[1] = args[++ i];
      }else if ("-b".equals(args[i])){
        options[2] = args[++ i];
      }else if ("-i".equals(args[i])){
        options[3] = args[++ i];
      }else{
        throw new NodoException("Uso: nodo -p <puerto> -c <conocidos> -b <biblioteca> -i <ID del nodo>");
      }
    }

    //Options are kept in an array to ensure each one has been specified
    if (null == options[0] || null == options[1] || null == options[2] || null == options[3]){
      throw new NodoException("Uso: nodo -p <puerto> -c <conocidos> -b <biblioteca> -i <ID del nodo>");
    }

    int port;
    try{
      port = Integer.parseInt(options[0]);
    }catch(NumberFormatException e){
      throw new NodoException("Puerto '" + options[0] + "' inválido: " + e.getMessage());
    }

    ArrayList<Host> knownHosts;
    try{
      knownHosts = getKnownHosts(options[1]);
    }catch(NodoException e){
      throw new NodoException("Error cargando el archivo de hosts conocidos '" + options[1] + "': " + e.getMessage());
    }

    ArrayList<Track> library;
    try{
      library = getLibrary(options[3], options[2]);
    }catch(NodoException e){
      throw new NodoException("Error cargando el archivo de librería '" + options[2] + "': " + e.getMessage());
    }

    return new Nodo(err, options[3], port, knownHosts, library);
  }

  /**
   * Given the file name of a file of known hosts, load them
   * It is expected each line of this file contains a hostname/ip
   * 
   * @param knownHostsFileName known hosts file name to load
   */
  private static ArrayList<Host> getKnownHosts(String knownHostsFileName)
    throws NodoException
  {
    ArrayList<Host> knownHosts = new ArrayList<Host>();
    BufferedReader hosts;
    try{
      hosts = new BufferedReader(new FileReader(knownHostsFileName));
    }catch(FileNotFoundException e){
      throw new NodoException("El archivo no existe: " + e.getMessage());
    }

    while (true){
      String hostLine;
      try{
        hostLine = hosts.readLine();
      }catch(IOException e){
        throw new NodoException("Error de lectura: " + e.getMessage());
      }

      if (null == hostLine){
        break;
      }

      try{
        knownHosts.add(new Host(hostLine));
      }catch(HostException e){
        throw new NodoException("Error creando host '" + hostLine + "': " + e.getMessage());
      }
    }

    return knownHosts;
  }

  /**
   * Given the file name of a file of an xspf file, load it
   * The path to each file specified must be valid
   * 
   * @param node      host to which this track belongs
   * @param libraryFileName library file name
   */
  private static ArrayList<Track> getLibrary(String nodeName, String libraryFileName)
    throws NodoException
  {
    ArrayList<Track> library = new ArrayList<Track>();
    Document doc;
    try{
      doc = DocumentBuilderFactory
              .newInstance()
              .newDocumentBuilder()
              .parse(new File(libraryFileName));
    }catch (ParserConfigurationException e) {
      throw new NodoException("Error de configuración: " + e.getMessage());
    }catch (IOException e) {
      throw new NodoException("Error abriendo el archivo: " + e.getMessage());
    }catch (SAXParseException e) {
      throw new NodoException("Error en la línea" + e.getLineNumber() + "' uri '" + e.getSystemId() + "': " + e.getMessage());
    }catch (SAXException e) {
      throw new NodoException(e.getMessage());
    }

    doc.getDocumentElement().normalize();

    if (!"playlist".equals(doc.getDocumentElement().getNodeName())){
      throw new NodoException("Nodo raíz es '" + doc.getDocumentElement().getNodeName() + "' y se esperaba 'playlist'");
    }

    Node trackListNode = doc.getElementsByTagName("trackList").item(0);

    //trackList node must exist
    if (null == trackListNode){
      throw new NodoException("Nodo trackList no existe");
    }

    //Type validation
    if (Node.ELEMENT_NODE != trackListNode.getNodeType()){
      throw new NodoException(
        "El nodo trackList es de tipo " + trackListNode.getNodeType() +
        " y se esperaba " + Node.ELEMENT_NODE + " (Node.ELEMENT_NODE)");
    }

    NodeList trackNodeList = trackListNode.getChildNodes();
    int trackLength = trackNodeList.getLength();
    for (int i = 0; i < trackLength; ++ i){
      Node trackNode = trackNodeList.item(i);
      if ("#text".equals(trackNode.getNodeName())){
        continue;
      }
      try{
        library.add(getLibraryTrack(nodeName, trackNode));
      }catch(NodoException e){
        throw new NodoException("Error en el nodo #" + i +
          " de trackList con nombre '" + trackNode.getNodeName() + "': " + e.getMessage());
      }
    }

    return library;
  }

  /**
   * Given a Node, extract the information of the track it contains
   * 
   * @param node      host to which this track belongs
   * @param trackNode node with location, creator and title
   */
  private static Track getLibraryTrack(String nodeName, Node trackNode)
    throws NodoException
  {

    //Type validation
    if (Node.ELEMENT_NODE != trackNode.getNodeType()){
      throw new NodoException(
        "El nodo es de tipo " + trackNode.getNodeType() +
        " y se esperaba " + Node.ELEMENT_NODE + " (Node.ELEMENT_NODE)");
    }

    //Name validation
    if (!"track".equals(trackNode.getNodeName())){
      throw new NodoException("Se esperaba un nodo de nombre 'track'");
    }

    try{
      return new Track(
        getTrackDataNode(trackNode, "location"),
        getTrackDataNode(trackNode, "title"),
        getTrackDataNode(trackNode, "creator"), nodeName);
    }catch(TrackException e){
      throw new NodoException("Error creando la canción: " + e.getMessage());
    }
  }

  /**
   * Extract a specific key of the track
   * 
   * @param trackNode track with information
   * @param name name of the key to extract of the track
   */
  private static String getTrackDataNode(Node trackNode, String name)
    throws NodoException
  {
    try{
      return ((Element) trackNode).getElementsByTagName(name).item(0).getChildNodes().item(0).getNodeValue();
    }catch(DOMException e){
      throw new NodoException("Error leyendo el valor '" + name +
        "' del nodo '" + trackNode.getNodeName() + "': " + e.getMessage());
    }

  }



  //Error PrintStream
  private PrintStream err;

  //This node's name
  private String nodeName;

  //Port to which this node is hearing
  private int port;

  //List of known hosts to which this node can connect
  private ArrayList<Host> knownHosts;

  //Songs this node knows provides
  private ArrayList<Track> library;

  /**
   * Constructor
   * 
   * This class acts as node. Given a port, this node can hear requests through it. It provides
   * all songs specified in the library. It can also perform recursive requests to other 
   * knownHosts when the user performs one. This node is identified by the nodeName String
   * 
   * @param err         Error PrintStream for errors reported in request handlers
   * @param node        This host
   * @param port        Port to hear on
   * @param knownHosts  Other nodes known by this node
   * @param library     Tracks which are provided by this node
   */
  public Nodo(PrintStream err, String nodeName, int port, ArrayList<Host> knownHosts, ArrayList<Track> library)
    throws NodoException
  {
    if (port < 1024 || 65535 < port){
      throw new NodoException("Número de puerto " + port + " inválido");
    }

    this.err = err;
    this.nodeName = nodeName;
    this.port = port;
    this.knownHosts = knownHosts;
    this.library = library;
  }

  /**
   * Hears to the specified port and fires a thread if a client
   * contacts us
   */
  private void hear()
    throws NodoException
  {
    //Initialize server
    ServerSocket s;
    try{
      s = new ServerSocket(port);
    }catch(IOException e){
      throw new NodoException("Error generando el socket servidor: " + e.getMessage());
    }

    while(true){

      //Hear to a connection
      Socket client;
      try{
        client = s.accept();
      }catch(IOException e){
        throw new NodoException("Error aceptando una conexión de servidor: " + e.getMessage());
      }

      //Fire thread to attend request
      Thread t = new Thread(new NodoThread(err, client, nodeName, port, knownHosts, library));
      try{
        t.start();
      }catch(IllegalThreadStateException e){
        throw new NodoException("Error ejecutando hilo para atender solicitud: " + e.getMessage());
      }
    }
  }

  public void NodoThreadException(String message)
    throws NodoException
  {
    throw new NodoException("Mensaje de error" + message);
  }

  /**
   * Creates a string representation of object for debugging purposes
   */
  public String toString(){

    String s = "##################### PORT #####################\n" + port + '\n' +
               "##################### HOSTS #####################\n";
    for (Host h : knownHosts){
      s += h.toString() + '\n';
    }
    s += "##################### LIBRARY #####################\n";
    for (Track t : library){
      s += t.toString() + '\n';
    }
    s += "##################### NODE #####################\n" + nodeName;
    return s;
  }
}